面對龐大架構,官方建議使用 Blueprints 將程式碼拆分成不同的模組(modules)
Blueprint 是一種把關聯程式和 view 組織起來的方式
和直過把 view 和其他程式直接注冊到應用的方式不同,直接是把它們注冊到Blueprints
然後在工廠函數中把 Blueprint 註冊到應用中
在我們的練習 Flaskr 中有兩個 Blueprint,一個用於認證功能,另一個用於部落格文章管理
每個 Blueprint 的程式都在一個獨立的的 module 中
使用部落格首先需要認證,因此我們先寫認證的 Blueprint
flaskr/auth.py
import functools
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
from flaskr.db import get_db
bp = Blueprint('auth', __name__, url_prefix='/auth')
這裡創建了一個名稱為auth
的 Blueprint
和應用物件一樣,Blueprint 需要知道是在哪裡定義的,因此把 __name__
作為函數的第二個參數
url_prefix
會添加到所有與該 Blueprint 關聯的 URL 前面
flaskr/init.py
修改檔案,使用 app.register_blueprint() 導入並注冊剛剛建立的 auth.bp
Blueprint
把新的程式放在工廠函數的尾部回傳應用之前!
def create_app():
app = ...
# existing code omitted
from . import auth
app.register_blueprint(auth.bp)
return app
認證的 Blueprint 將包括新用戶註冊、登入和登出的 view
當訪問/auth/register
URL 時,register view 會回傳註冊表單的 HTML 頁面
當用戶提交表單時,view 會驗證表單內容,接著根據註冊結果
註冊成功則建立新用戶並顯示登錄頁面,否則顯示表單並顯示一個錯誤訊息
底下是 view function 的程式內容
flaskr/auth.py
@bp.route('/register', methods=('GET', 'POST'))
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
if not username:
error = 'Username is required.'
elif not password:
error = 'Password is required.'
if error is None:
try:
db.execute(
"INSERT INTO user (username, password) VALUES (?, ?)",
(username, generate_password_hash(password)),
)
db.commit()
except db.IntegrityError:
error = f"User {username} is already registered."
else:
return redirect(url_for("auth.login"))
flash(error)
return render_template('auth/register.html')
這個register view function
做了以下的事情
@bp.route 關聯了 URL /register 和 register 的 view function
當 Flask 收到一個指向 /auth/register 的 request 時會呼叫 register view function
並把其返回值作為 response
如果使用者送出表單,那麼 request.method 將會是POST
這個情況下會進行輸入內容的驗證
request.form 是一個特殊類型的 dict,其對應了提交表單的鍵和值
表單中會求使用者會輸入username
和password
,所以要驗證username
和password
不為空
如果表單驗證通過,則寫入新的使用者資料
db.execute 使用了帶有「?
」佔位符的 SQL 查詢語句
佔位符可以代替後面的元組參數中相應的值
使用佔位符的好處是會自動幫你轉譯輸入值,以防止 SQL 注入攻擊
為了安全原因,不能把密碼明文儲存在資料庫中
使用 generate_password_hash() 將輸入的密碼進行 hash
而 db.execute 只會執行 SQL 指令
要將指令提交至 SQL,要使用 db.commit() 才會真的執行前面的 SQL query
如果username
已經存在,造成無法寫入的情況下
會發生sqlite3.IntegrityError
,這時候會建立一條錯誤提示訊息
使用者資料建立後會跳轉到登入頁面,透過 url_for() 產生對應路由方法函式的 URL
比起直接寫死 URL,這麼做的好處是如果之後需要修改對應的 URL,則不需要一個一個找出來改
使用 redirect() 直接跳轉到指定的 URL,也就是登入頁
如果驗證失敗,透過flash() 可以在渲染的模塊時候向使用者顯示一個錯誤訊息
使用者一開始打開auth/register
時,或是註冊出錯時應該顯示一個註冊的表單
呼叫 render_template() 會渲染一個包含 HTML 的模板,在下一節會學習如何寫這個模板
這個 view 和上面註冊的模式一樣
flaskr/auth.py
@bp.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
user = db.execute(
'SELECT * FROM user WHERE username = ?', (username,)
).fetchone()
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user['password'], password):
error = 'Incorrect password.'
if error is None:
session.clear()
session['user_id'] = user['id']
return redirect(url_for('index'))
flash(error)
return render_template('auth/login.html')
與register
有一點點不同之處
fetchone() 會據查詢回傳一筆紀錄,如果查詢沒有結果,則回傳None
後面會用到 fetchall() 則會回傳所有查詢結果的列表
check_password_hash() 會以相同的方式對密碼進行 hash 密碼並比較查詢結果是否正確
session 是一個 dict,用於儲存橫跨請求的值
當登入驗證成功後,使用者的id
被儲存於一個新的 session 中
資料被儲存到一個向瀏覽器發送的 cookie 中,而瀏覽器在後繼發送的請求中會帶上該 cookie
Flask 會對資料進行簽章,以防數據被篡改
現在使用者 id 已被儲存在 session 中,可以在後續的 request 中使用
如果使用者已經登入,那麼他的使用者資料應該被載入並且在其他 view 裡被使用
flaskr/auth.py
@bp.before_app_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (user_id,)
).fetchone()
bp.before_app_request() 註冊一個在 view function 之前運行的函數
不論 URL 是什麼,load_logged_in_user
都會檢查使用者 id 是否已經儲存在 session 中
並從資料庫中取得使用者資料,然後儲存在 g.user 中,並且會持續存在
如果沒有使用者 id ,或者 id 查詢結果不存在,那g.user
將會是None
登出的時候需要把使用者 id 從 session 中移除
然後 load_logged_in_user
就不會在後續的請求中載入使用者資料了
flaskr/auth.py
@bp.route('/logout')
def logout():
session.clear()
return redirect(url_for('index'))
因為要登入以後才能建立、編輯和刪除文章
在每個 view 中可以使用裝飾器
來完成這個工作
flaskr/auth.py
def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))
return view(**kwargs)
return wrapped_view
裝飾器回傳一個新的 view,包含了傳遞給裝飾器的原始 view
新的函數檢查使用者是否登入
如果已經登入,那麼就繼續正常執行原本的 view
否則就跳轉到登入頁!之後會在部落格的 view 中使用這個裝飾器
函數 url_for()
根據 view 的名稱產生 URL
和 view 相關聯的名稱亦稱為 endpoint
預設情況下,endpoint 名稱與 view 的函數名稱
相同
For example, the hello() view that was added to the app factory earlier in the tutorial has the name 'hello' and can be linked to with url_for('hello'). If it took an argument, which you’ll see later, it would be linked to using url_for('hello', who='World').
例如,之前被加入應用工廠hello()
的 view 為'hello'
,可以使用url_for('hello')
來連接
之後會遇到 view 有參數,那麼可使用url_for('hello', who='World')
連接
When using a blueprint, the name of the blueprint is prepended to the name of the function, so the endpoint for the login function you wrote above is 'auth.login' because you added it to the 'auth' blueprint.
當使用 blueprint 的時候,blueprint 的名稱會添加到函數名稱的前面
上面寫的 login 函數 endpoint 為'auth.login'
,因為你把他加在 'auth'
的 blueprint 中
今天這篇是真的長,主委沒拆成多篇水一天很有誠意的吧